package jxtend.processor.eclipse.visitor.handler.impl;

import static java.lang.Character.isUpperCase;
import static java.lang.Character.toUpperCase;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.NoSuchElementException;

import jxtend.annotation.keyword.onRead;
import jxtend.annotation.keyword.onWrite;
import jxtend.annotation.keyword.property;
import jxtend.processor.eclipse.utils.EclipseProcessorUtils;
import jxtend.processor.eclipse.visitor.handler.ASTNodeHandlerVisitorContext;
import jxtend.processor.eclipse.visitor.handler.AbstractASTNodeVisitorHandler;
import jxtend.processor.eclipse.visitor.handler.ProblemSeverity;
import jxtend.processor.reflect.ReflectField;

import org.eclipse.jdt.core.compiler.CharOperation;
import org.eclipse.jdt.core.dom.Modifier;
import org.eclipse.jdt.internal.compiler.ast.ASTNode;
import org.eclipse.jdt.internal.compiler.ast.AbstractMethodDeclaration;
import org.eclipse.jdt.internal.compiler.ast.Annotation;
import org.eclipse.jdt.internal.compiler.ast.Argument;
import org.eclipse.jdt.internal.compiler.ast.ArrayInitializer;
import org.eclipse.jdt.internal.compiler.ast.Assignment;
import org.eclipse.jdt.internal.compiler.ast.Expression;
import org.eclipse.jdt.internal.compiler.ast.FieldDeclaration;
import org.eclipse.jdt.internal.compiler.ast.FieldReference;
import org.eclipse.jdt.internal.compiler.ast.MarkerAnnotation;
import org.eclipse.jdt.internal.compiler.ast.MemberValuePair;
import org.eclipse.jdt.internal.compiler.ast.MethodDeclaration;
import org.eclipse.jdt.internal.compiler.ast.NameReference;
import org.eclipse.jdt.internal.compiler.ast.NormalAnnotation;
import org.eclipse.jdt.internal.compiler.ast.QualifiedTypeReference;
import org.eclipse.jdt.internal.compiler.ast.ReturnStatement;
import org.eclipse.jdt.internal.compiler.ast.SingleMemberAnnotation;
import org.eclipse.jdt.internal.compiler.ast.SingleNameReference;
import org.eclipse.jdt.internal.compiler.ast.Statement;
import org.eclipse.jdt.internal.compiler.ast.ThisReference;
import org.eclipse.jdt.internal.compiler.ast.TrueLiteral;
import org.eclipse.jdt.internal.compiler.ast.TypeDeclaration;
import org.eclipse.jdt.internal.compiler.ast.TypeReference;
import org.eclipse.jdt.internal.compiler.classfmt.ClassFileConstants;
import org.eclipse.jdt.internal.compiler.lookup.Binding;
import org.eclipse.jdt.internal.compiler.lookup.Scope;
import org.eclipse.jdt.internal.compiler.lookup.TypeIds;

@SuppressWarnings("restriction")
public class PropertyHandler extends AbstractASTNodeVisitorHandler {

	private static final char[] PRIMITIVE_BOOLEAN_TYPE_NAME_AS_CHAR_ARRAY = "boolean".toCharArray();
	private static final char[] VALUE_ATTRIBUTE = "value".toCharArray();
	private static final char[] ON_PARAM_ATTRIBUTE = "onParam".toCharArray();

	@Override
	public void beginVisit(ASTNodeHandlerVisitorContext context, ASTNode node, Scope scope) {
		if (!(node instanceof QualifiedTypeReference)
			    || !EclipseProcessorUtils.compoundNameEquals(((QualifiedTypeReference) node).tokens, property.class)
				|| !(context.getParentNode(0) instanceof Annotation)
				|| context.getParentNode(0) instanceof SingleMemberAnnotation
				|| !(context.getParentNode(1) instanceof FieldDeclaration)) {
			return;
		}

		FieldDeclaration fieldDeclaration = (FieldDeclaration) context.getParentNode(1);
		Annotation propertyAnnotation = (Annotation) context.getParentNode();
		if (!(propertyAnnotation instanceof MarkerAnnotation) && !(propertyAnnotation instanceof NormalAnnotation)) {
			return;
		}

		TypeDeclaration typeDeclaration = (TypeDeclaration) context.getParentNode(2);

		boolean writeOnly = isMemberValueIsTrue(propertyAnnotation, "writeOnly");

		fieldDeclaration.modifiers |= ClassFileConstants.AccPrivate;
		if (fieldDeclaration.binding != null) {
			fieldDeclaration.binding.modifiers |= ClassFileConstants.AccPrivate;
		}

		MethodDeclaration readMethod = null;
		boolean needsReadMethod = !writeOnly;
		AbstractMethodDeclaration existingReadMethod =
				getMethod(typeDeclaration, propertyReadMethodName(fieldDeclaration));

		if (needsReadMethod && existingReadMethod == null) {
			readMethod =
				generateGetterForAnnotatedField(context, typeDeclaration, fieldDeclaration, propertyAnnotation);
		}

		Annotation onReadAnnotation =
				EclipseProcessorUtils.findQualifiedAnnotation(fieldDeclaration.annotations, onRead.class);
		if (onReadAnnotation != null) {
			if (existingReadMethod != null) {
				context.recordProblemForNode(ProblemSeverity.ERROR, typeDeclaration, 
						onReadAnnotation.sourceStart, onReadAnnotation.sourceEnd,
						"The @onRead annotation not allowed in combination with explicit read method");
			} else if (readMethod == null) {
				context.recordProblemForNode(ProblemSeverity.ERROR, typeDeclaration, 
						onReadAnnotation.sourceStart, onReadAnnotation.sourceEnd,
						"The @onRead annotation not applicable for write-only properties");
			} else if (onReadAnnotation instanceof MarkerAnnotation) {
				context.recordProblemForNode(ProblemSeverity.ERROR, typeDeclaration, 
						onReadAnnotation.sourceStart, onReadAnnotation.sourceEnd,
						"Missing a value for the @onRead annotation");
			} else {
				Expression valueAttribute = getAnnotationAttribute(onReadAnnotation, VALUE_ATTRIBUTE);

				readMethod.annotations = extractAnnotations(context, typeDeclaration, valueAttribute);
			}
		}

		if (readMethod != null) {
			context.appendMethod(typeDeclaration, readMethod);
		}
		
		boolean fieldHasFinalModifier = ((fieldDeclaration.modifiers & ClassFileConstants.AccFinal) != 0);
		boolean readOnly = fieldHasFinalModifier || isMemberValueIsTrue(propertyAnnotation, "readOnly");

		boolean needsWriteMethod = !readOnly;
		AbstractMethodDeclaration existingWriteMethod =
				getMethod(typeDeclaration, propertyWriteMethodName(fieldDeclaration));

		MethodDeclaration writeMethod = null;
		if (needsWriteMethod && existingWriteMethod == null) {
			writeMethod =
				generateSetterForAnnotatedField(context, typeDeclaration, fieldDeclaration, propertyAnnotation);
		}

		Annotation onWriteAnnotation =
				EclipseProcessorUtils.findQualifiedAnnotation(fieldDeclaration.annotations, onWrite.class);

		if (onWriteAnnotation != null) {
			if (existingWriteMethod != null) {
				context.recordProblemForNode(ProblemSeverity.ERROR, typeDeclaration, 
						onWriteAnnotation.sourceStart, onWriteAnnotation.sourceEnd,
						"The @onWrite annotation not allowed in combination with explicit write method");
			} else if (writeMethod == null) {
				context.recordProblemForNode(ProblemSeverity.ERROR, typeDeclaration, 
						onWriteAnnotation.sourceStart, onWriteAnnotation.sourceEnd,
						"The @onWrite annotation not applicable for read-only properties");
			} else if (onReadAnnotation instanceof MarkerAnnotation) {
				context.recordProblemForNode(ProblemSeverity.ERROR, typeDeclaration, 
						onWriteAnnotation.sourceStart, onWriteAnnotation.sourceEnd,
						"Missing a value for the @onWrite annotation");
			} else {
				Expression valueAttribute = findAnnotationAttribute(onWriteAnnotation, VALUE_ATTRIBUTE);
				Expression onParamAttribute = findAnnotationAttribute(onWriteAnnotation, ON_PARAM_ATTRIBUTE);

				if (valueAttribute != null) {
					writeMethod.annotations = extractAnnotations(context, typeDeclaration, valueAttribute);
				}
				if (onParamAttribute != null) {
					writeMethod.arguments[0].annotations =
						extractAnnotations(context, typeDeclaration, onParamAttribute);
				}
			}
		}

		if (writeMethod != null) {
			context.appendMethod(typeDeclaration, writeMethod);
		}

		if ((needsReadMethod && existingReadMethod != null && needsWriteMethod && existingWriteMethod != null)
				|| (needsReadMethod && existingReadMethod != null && !needsWriteMethod && existingWriteMethod == null)
				|| (!needsReadMethod && existingReadMethod == null
						&& needsWriteMethod && existingWriteMethod != null)) {
			context.recordProblemForNode(ProblemSeverity.ERROR, context.findParentNode(TypeDeclaration.class),
					propertyAnnotation.sourceStart, propertyAnnotation.sourceEnd,
					"Useless @property annotation; all required property read/write methods are explicitly coded");
		} else if (readOnly && existingWriteMethod != null) {
			context.recordProblemForNode(ProblemSeverity.ERROR, context.findParentNode(TypeDeclaration.class),
					existingWriteMethod.sourceStart, existingWriteMethod.sourceEnd,
					"Write method not allowed for read-only property");
		} else if (writeOnly && existingReadMethod != null) {
			context.recordProblemForNode(ProblemSeverity.ERROR, context.findParentNode(TypeDeclaration.class),
					existingReadMethod.sourceStart, existingReadMethod.sourceEnd,
					"Write method not allowed for write-only property");
		}

		return;
	}

	private Annotation[] extractAnnotations(ASTNodeHandlerVisitorContext context,
			TypeDeclaration typeDeclaration, Expression annotationValue) {
		if (annotationValue instanceof ArrayInitializer) {
			Expression[] arrayInitializerExpression = ((ArrayInitializer) annotationValue).expressions;
			if (arrayInitializerExpression != null) {
				List<Annotation> annotationList = new ArrayList<Annotation>();
				for (Expression expression : arrayInitializerExpression) {
					if ((expression != null) && !(expression instanceof Annotation)) {
						context.recordProblemForNode(ProblemSeverity.ERROR, typeDeclaration, 
								expression.sourceStart, expression.sourceEnd,
								"Only annotation types are allowed here");
					} else {
						annotationList.add((Annotation) expression);
					}
				}

				return annotationList.toArray(new Annotation[annotationList.size()]);
			}
		} else if (annotationValue instanceof Annotation) {
			return new Annotation[] { (Annotation) annotationValue };
		} else if (annotationValue != null) {
			@SuppressWarnings({ "unchecked", "rawtypes" }) ReflectField<ASTNode, Binding> bindingField =
				ReflectField.getPublicField((Class) annotationValue.getClass(), Binding.class, "binding");
			Binding binding = bindingField.get(annotationValue);

			if ((binding != null) && binding.isValidBinding()) {
				context.recordProblemForNode(ProblemSeverity.ERROR, typeDeclaration, 
						annotationValue.sourceStart, annotationValue.sourceEnd,
						"Only annotation types are allowed here");
			}
		}

		return null;
	}

	private MethodDeclaration generateGetterForAnnotatedField(ASTNodeHandlerVisitorContext context,
			TypeDeclaration typeDeclaration, FieldDeclaration fieldDeclaration, Annotation propertyAnnotation) {
		long fieldPos = encodeSourceStartAndEndPosition(propertyAnnotation);

		MethodDeclaration methodDeclaration = new MethodDeclaration(typeDeclaration.compilationResult);
		methodDeclaration.modifiers = ClassFileConstants.AccPublic;
		methodDeclaration.returnType = fieldDeclaration.type;
		methodDeclaration.selector =  propertyReadMethodName(fieldDeclaration).toCharArray();

		methodDeclaration.declarationSourceStart =
			methodDeclaration.sourceStart =
			methodDeclaration.modifiersSourceStart =
				methodDeclaration.bodyStart =
					propertyAnnotation.sourceStart;
		methodDeclaration.declarationSourceEnd =
			methodDeclaration.sourceEnd =
				propertyAnnotation.sourceEnd;

		FieldReference fieldReference = new FieldReference(fieldDeclaration.name, fieldPos);
		fieldReference.receiver = new ThisReference(methodDeclaration.sourceStart, methodDeclaration.sourceEnd);
		fieldReference.sourceStart = methodDeclaration.sourceStart;
		fieldReference.sourceEnd = fieldReference.statementEnd = methodDeclaration.sourceEnd;

		Statement returnStatement = 
			new ReturnStatement(fieldReference, fieldDeclaration.sourceStart, fieldDeclaration.sourceEnd);
		returnStatement.sourceStart = methodDeclaration.sourceStart;
		returnStatement.sourceEnd = methodDeclaration.sourceEnd;

		methodDeclaration.statements = new Statement[] { returnStatement };

		return methodDeclaration;
	}

	private MethodDeclaration generateSetterForAnnotatedField(ASTNodeHandlerVisitorContext context, TypeDeclaration typeDeclaration,
			FieldDeclaration fieldDeclaration, Annotation propertyAnnotation) {
		long fieldPos = encodeSourceStartAndEndPosition(propertyAnnotation);

		MethodDeclaration methodDeclaration = new MethodDeclaration(typeDeclaration.compilationResult);
		methodDeclaration.modifiers = ClassFileConstants.AccPublic;
		methodDeclaration.returnType = TypeReference.baseTypeReference(TypeIds.T_void, 0);
		methodDeclaration.selector =  propertyWriteMethodName(fieldDeclaration).toCharArray();

		methodDeclaration.declarationSourceStart =
			methodDeclaration.sourceStart =
			methodDeclaration.modifiersSourceStart =
				methodDeclaration.bodyStart =
					propertyAnnotation.sourceStart;
		methodDeclaration.declarationSourceEnd =
			methodDeclaration.sourceEnd =
				propertyAnnotation.sourceEnd;

		Argument argument = new Argument(fieldDeclaration.name, fieldPos, fieldDeclaration.type, Modifier.FINAL);
		argument.declarationSourceStart = argument.sourceStart = argument.modifiersSourceStart =
			methodDeclaration.declarationSourceStart;
		argument.declarationSourceEnd = argument.sourceEnd = methodDeclaration.declarationSourceEnd;
		methodDeclaration.arguments = new Argument[] { argument };

		methodDeclaration.statements = new Statement[] {
					createFieldAssignment(fieldDeclaration, argument, fieldPos, methodDeclaration) };

		return methodDeclaration;
	}

	private static long encodeSourceStartAndEndPosition(ASTNode astNode) {
		int sourceStart = astNode.sourceStart;
		int sourceEnd = astNode.sourceEnd;
		return ((long) sourceStart << 32) | sourceEnd;
	}

	private static String propertyReadMethodName(FieldDeclaration field) {
		String methodNamePrefix = isPrimitiveBoolean(field.type) ? "is" : "get";

		return propertyMethodName(field, methodNamePrefix);
	}

	private static String propertyWriteMethodName(FieldDeclaration field) {
		return propertyMethodName(field, "set");
	}

	private static String propertyMethodName(FieldDeclaration field, String methodNamePrefix) {
		if ((field.name.length > 2) && isUpperCase(field.name[1])) {
			return methodNamePrefix + new String(field.name);
		} else {
			return methodNamePrefix
				+ String.valueOf(toUpperCase(field.name[0])) + String.copyValueOf(field.name, 1, field.name.length - 1); 
		}
	}

	private static boolean isPrimitiveBoolean(TypeReference typeReference) {
		char[][] typeName = typeReference.getTypeName();

		return (typeName.length == 1) && Arrays.equals(PRIMITIVE_BOOLEAN_TYPE_NAME_AS_CHAR_ARRAY, typeName[0]);
	}

	private static boolean isMemberValueIsTrue(Annotation annotation, String memberValueName) {
		MemberValuePair[] memberValuePairs = annotation.memberValuePairs();

		char[] expectedMemberValueName = memberValueName.toCharArray();
		for (MemberValuePair memberValuePair : memberValuePairs) {
			if (CharOperation.equals(expectedMemberValueName, memberValuePair.name)
					&& (memberValuePair.value instanceof TrueLiteral)) {
				return true;
			}
		}

		return false;
	}

	private static Expression findAnnotationAttribute(Annotation annotation, char[] attributeName) {
		MemberValuePair[] memberValuePairs = annotation.memberValuePairs();

		for (MemberValuePair memberValuePair : memberValuePairs) {
			if (CharOperation.equals(attributeName, memberValuePair.name)) {
				return memberValuePair.value;
			}
		}

		return null;
	}

	private static Expression getAnnotationAttribute(Annotation annotation, char[] attributeName) {
		Expression result = findAnnotationAttribute(annotation, attributeName);

		if (result == null) {
			throw new NoSuchElementException("Annotation doesn't have a value attribute");
		}
		
		return result;
	}

	private static Assignment createFieldAssignment(FieldDeclaration fieldDeclaration, Argument argument, long pos,
			AbstractMethodDeclaration methodDeclaration) {
		FieldReference fieldReference = new FieldReference(fieldDeclaration.name, pos);
		fieldReference.receiver = new ThisReference(fieldDeclaration.sourceStart, fieldDeclaration.sourceEnd);
		fieldReference.sourceStart = fieldDeclaration.sourceStart;
		fieldReference.sourceEnd = fieldReference.statementEnd = fieldDeclaration.sourceEnd;

		NameReference paramReference = new SingleNameReference(fieldDeclaration.name, pos);
		paramReference.sourceStart = fieldDeclaration.sourceStart;
		paramReference.sourceEnd = paramReference.statementEnd = fieldDeclaration.sourceEnd;

		Assignment result = new Assignment(fieldReference, paramReference, methodDeclaration.declarationSourceEnd);
		result.sourceStart = fieldDeclaration.sourceStart;
		result.sourceEnd = fieldDeclaration.sourceEnd;

		return result;
	}
	
	private AbstractMethodDeclaration getMethod(TypeDeclaration typeDeclaration, String methodName) {
		AbstractMethodDeclaration[] methods = typeDeclaration.methods;
		if (methods == null) {
			return null;
		}

		for (AbstractMethodDeclaration method : methods) {
			if (CharOperation.equals(methodName.toCharArray(), method.selector)) {
				return method;
			}
		}

		return null;
	}
}
